//@author A0101119Y
#include "GCalParser.h"

using namespace Poco::JSON;
using namespace Poco::Dynamic;

using namespace Twentyfifth;
using namespace std;


const string GCalParser::LABEL_RESPONSE_EVENTS = "items";
const string GCalParser::LABEL_RESPONSE_CALENDARS = "items";
const string GCalParser::LABEL_ITEMS_ID = "id";
const string GCalParser::LABEL_ITEMS_DESCRIPTION = "summary";
const string GCalParser::LABEL_ITEMS_VENUE = "location";
const string GCalParser::LABEL_ITEMS_START = "start";
const string GCalParser::LABEL_ITEMS_END = "end";
const string GCalParser::LABEL_ITEMS_DATE = "date";
const string GCalParser::LABEL_ITEMS_DATETIME = "dateTime";
const string GCalParser::LABEL_ITEMS_TIMEZONE = "timeZone";
const string GCalParser::LABEL_ITEMS_LAST_UPDATED = "updated";
const string GCalParser::LABEL_ITEMS_STATUS = "status";

const regex GCalParser::REGEX_DATETIME_GOOGLE("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2}).*");
const regex GCalParser::REGEX_DATE_GOOGLE("(\\d{4})-(\\d{2})-(\\d{2})$");
const string GCalParser::VALUE_DATETIME_GOOGLE = "y-m-dTh:i:s";
const string GCalParser::VALUE_DATE_GOOGLE = "y-m-d";
const string GCalParser::VALUE_TIMESTAMP_DAY = "d";
const string GCalParser::VALUE_TIMESTAMP_MONTH = "m";
const string GCalParser::VALUE_TIMESTAMP_YEAR = "y";
const string GCalParser::VALUE_TIMESTAMP_HOUR = "h";
const string GCalParser::VALUE_TIMESTAMP_MINUTE = "i";
const string GCalParser::VALUE_TIMESTAMP_SECOND = "s";
const string GCalParser::VALUE_ITEMS_DELETED_FALSE = "confirmed";
const string GCalParser::VALUE_ITEMS_DELETED_TRUE = "cancelled";


GCalParser::GCalParser() {
	VALUE_TIMEZONE = BLANK_STRING;
}

//
// Individual task parsers
//

Task GCalParser::parseObjectToTask(Object::Ptr object) {
	Task t;
	t.setTaskType(Task::TaskType::TIMED_TASK);

	string gcalid = object->getValue<string>(LABEL_ITEMS_ID);
	t.setGCalId(gcalid);

	DateTime lastUpdated = parseDtLastUpdatedToLocal(object->getValue<string>(LABEL_ITEMS_LAST_UPDATED));
	t.setLastUpdated(lastUpdated);

	string status = object->getValue<string>(LABEL_ITEMS_STATUS);
	if (status == VALUE_ITEMS_DELETED_FALSE) {
		t.setDeleted(false);
		string description = object->getValue<string>(LABEL_ITEMS_DESCRIPTION);
		t.setDescription(description);
		
		try {
			string venue = object->getValue<string>(LABEL_ITEMS_VENUE);
			t.setVenue(venue);
		}
		catch (Poco::InvalidAccessException&) {}
		
		DateTime start;
		DateTime end;
		try {
			start = parseDtGoogleToTask(object->getObject(LABEL_ITEMS_START)->getValue<string>(LABEL_ITEMS_DATETIME));
			end = parseDtGoogleToTask(object->getObject(LABEL_ITEMS_END)->getValue<string>(LABEL_ITEMS_DATETIME));
			t.setFullDay(false);
		}
		catch (Poco::Exception&) {
			start = parseDtGoogleToTask(object->getObject(LABEL_ITEMS_START)->getValue<string>(LABEL_ITEMS_DATE));
			end = parseDtGoogleToTask(object->getObject(LABEL_ITEMS_END)->getValue<string>(LABEL_ITEMS_DATE));
			t.setFullDay(true);
		}
		t.setStart(start);
		t.setEnd(end);
	} else {
		t.setDeleted(true);
	}
	return t;
}

Object GCalParser::parseTaskToObject(Task t) {
	Object object;

	if (t.getTaskType() == Task::TIMED_TASK) {
		Object dtStart;
		Object dtEnd;
		if (t.isFullDay()) {
			dtStart.set(LABEL_ITEMS_DATE, parseDtTaskToGoogle(t.getStart(), true));
			dtEnd.set(LABEL_ITEMS_DATE, parseDtTaskToGoogle(t.getEnd(), true));
		} else {
			dtStart.set(LABEL_ITEMS_DATETIME, parseDtTaskToGoogle(t.getStart()));
			dtStart.set(LABEL_ITEMS_TIMEZONE, VALUE_TIMEZONE);
			dtEnd.set(LABEL_ITEMS_DATETIME, parseDtTaskToGoogle(t.getEnd()));
			dtEnd.set(LABEL_ITEMS_TIMEZONE, VALUE_TIMEZONE);
		}
		object.set(LABEL_ITEMS_START, dtStart);
		object.set(LABEL_ITEMS_END, dtEnd);
	}

	object.set(LABEL_ITEMS_DESCRIPTION, t.getDescription());
	
	object.set(LABEL_ITEMS_VENUE, t.getVenue());
	
	bool isDeleted = t.getDeleted();
	if (isDeleted) {
		object.set(LABEL_ITEMS_STATUS, VALUE_ITEMS_DELETED_TRUE);
	} else {
		object.set(LABEL_ITEMS_STATUS, VALUE_ITEMS_DELETED_FALSE);
	}
	
	return object;
}

Task GCalParser::parseResponseToTask(string response) {
	Task t;
	Parser parser;
	parser.parse(response);
	Var result = parser.result();
	Object::Ptr object = result.extract<Object::Ptr>();
	t = parseObjectToTask(object);
	return t;
}

//
// Group task parsers
//

vector<Task> GCalParser::parseArrayToTasks(Poco::JSON::Array::Ptr eventarray) {
	vector<Task> tasks;
	for (size_t i = 0; i < eventarray->size(); i++) {
		Task t;
		t = parseObjectToTask(eventarray->getObject(i));
		tasks.push_back(t);
	}
	return tasks;
}

Poco::JSON::Array GCalParser::parseTasksToArray(vector<Task> tasks) {
	Poco::JSON::Array jsonarray;
	for (size_t i = 0; i < tasks.size(); i++) {
		if (tasks[i].getTaskType() == Task::TIMED_TASK) {
			Object o;
			o = parseTaskToObject(tasks[i]);
			jsonarray.add(o);
		}
	}
	return jsonarray;
}

// Accepts either an object that contains the array of items, or the array itself.
vector<Task> GCalParser::parseResponseToTasks(string response) {
	vector<Task> tasks;
	Parser parser;
	parser.parse(response);
	Var result = parser.result();
	try {
		Object::Ptr object = result.extract<Object::Ptr>();	
		Poco::JSON::Array::Ptr eventArray = object->getArray(LABEL_RESPONSE_EVENTS);
		tasks = parseArrayToTasks(eventArray);
	}
	catch (Poco::BadCastException&) {
		Poco::JSON::Array::Ptr eventArray = result.extract<Poco::JSON::Array::Ptr>();
		tasks = parseArrayToTasks(eventArray);
	}
	return tasks;
}

Object GCalParser::parseMapToObject(map<string, string> m) {
	Object object;
	for (map<string, string>::iterator it = m.begin(); it != m.end(); it++) {
		string key = it->first;
		string value = it->second;
		object.set(key, value);
	}
	return object;
}

//
// Calendar parser
//

vector<map<string, string>> GCalParser::parseResponseToCalendars(string responseJson) {
	vector<map<string, string>> entries;
	map<string, string> responseMap = parseJsonToMap(responseJson);
	string calendarsJson = responseMap.at(LABEL_RESPONSE_CALENDARS);
	entries = parseJsonArrayToVector(calendarsJson);
	return entries;
}

//
// General JSON to map parsers
//

vector<map<string, string>> GCalParser::parseJsonArrayToVector(string json) {
	vector<map<string, string>> entries;
	Parser parser;
	parser.parse(json);
	Var result = parser.result();
	Poco::JSON::Array::Ptr userArray = result.extract<Poco::JSON::Array::Ptr>();
	for (size_t i = 0; i < userArray->size(); i++) {
		map<string, string> entry;
		stringstream ss;
		userArray->getObject(i)->stringify(ss);
		entry = parseJsonToMap(ss.str());
		entries.push_back(entry);
	}
	return entries;
}

map<string, string> GCalParser::parseJsonToMap(string json) {
	map<string, string> entries;
	vector<string> names;
	Parser parser;
	parser.parse(json);
	Var result = parser.result();
	Object::Ptr object = result.extract<Object::Ptr>();
	object->getNames(names);
	for (int i = 0; i < names.size(); i++) {
		string t = object->getValue<string>(names[i]);
		entries[names[i]] = t;
	}
	return entries;
}

//
// Date/time parsers
//

DateTime GCalParser::parseDtGoogleToTask(string dtGoogle) {
	DateTime dtTask;
	smatch match;
	if (regex_search(dtGoogle, match, REGEX_DATETIME_GOOGLE)) {
		dtTask.setYear(stoi(match[1]));
		dtTask.setMonth(stoi(match[2]));
		dtTask.setDay(stoi(match[3]));
		dtTask.setHour(stoi(match[4]));
		dtTask.setMinute(stoi(match[5]));
		dtTask.setSecond(stoi(match[6]));
	} else if (regex_search(dtGoogle, match, REGEX_DATE_GOOGLE)) {
		dtTask.setYear(stoi(match[1]));
		dtTask.setMonth(stoi(match[2]));
		dtTask.setDay(stoi(match[3]));
		dtTask.setHour(0);
		dtTask.setMinute(0);
		dtTask.setSecond(0);
	}
	return dtTask;
}

string GCalParser::parseDtTaskToGoogle(DateTime dtTask, bool isDateOnly) {
	string dtGoogle = VALUE_DATETIME_GOOGLE;
	if (isDateOnly) {
		dtGoogle = VALUE_DATE_GOOGLE;
	}
	size_t posYear = dtGoogle.find(VALUE_TIMESTAMP_YEAR);
	dtGoogle.replace(posYear, 1, to_string(dtTask.getYear()));

	size_t posMonth = dtGoogle.find(VALUE_TIMESTAMP_MONTH);
	int month = dtTask.getMonth();
	string monthStr = to_string(month);
	if (month < 10) {
		monthStr = "0" + monthStr;
	}
	dtGoogle.replace(posMonth, 1, monthStr);

	size_t posDay = dtGoogle.find(VALUE_TIMESTAMP_DAY);
	int day = dtTask.getDay();
	string dayStr = to_string(day);
	if (day < 10) {
		dayStr = "0" + dayStr;
	}
	dtGoogle.replace(posDay, 1, dayStr);

	if (isDateOnly) {
		return dtGoogle;
	}

	size_t posHour = dtGoogle.find(VALUE_TIMESTAMP_HOUR);
	int hour = dtTask.getHour();
	string hourStr = to_string(hour);
	if (hour < 10) {
		hourStr = "0" + hourStr;
	}
	dtGoogle.replace(posHour, 1, hourStr);

	size_t posMinute = dtGoogle.find(VALUE_TIMESTAMP_MINUTE);
	int minute = dtTask.getMinute();
	string minuteStr = to_string(minute);
	if (minute < 10) {
		minuteStr = "0" + minuteStr;
	}
	dtGoogle.replace(posMinute, 1, minuteStr);

	size_t posSecond = dtGoogle.find(VALUE_TIMESTAMP_SECOND);
	int second = dtTask.getSecond();
	string secondStr = to_string(second);
	if (second < 10) {
		secondStr = "0" + secondStr;
	}
	dtGoogle.replace(posSecond, 1, secondStr);

	return dtGoogle;
}

DateTime GCalParser::parseDtLastUpdatedToLocal(string lastUpdated) {
	DateTime dt;
	dt = parseDtGoogleToTask(lastUpdated);
	int timeOffsetFromUTC = Poco::Timezone::utcOffset();
	dt.offset(timeOffsetFromUTC);
	return dt;
}

void GCalParser::setTimeZone(string tz) {
	VALUE_TIMEZONE = tz;
}